上一篇的解答:
infix fun <T, Q, R> ((T) -> Q).pipe(anotherFun: (Q) -> R): (T) -> R {
return { x: T ->
anotherFun(this(x))
}
}
接下來要介紹另外兩個重要的概念了: Pure function 跟 Immutability,其實不只在 function programming ,在 Object oriented programming 中,如果好好掌握好 Pure function 與 Immutability 這兩個概念的話,會讓你寫的程式更好維護,也會產生更少的 bug。
怎樣 function 才能算是“純”的呢?要多純?這樣說好了,任何我們以前在學校學的數學函式都是 Pure function。Pure function 就是,給定一個輸入,會有唯一一個輸出的結果,不會因為任何因素(時空/星座/長相)影響到結果,如下方所示:
f(x) = x * 3
如果輸入是 1 ,那輸出永遠都會是 3 ,如果是入是3,那輸出會是9,不可能會是 6 或是其他的值。看到這邊你可能會覺得:蛤?當然每個 function 都一定會是 pure funciton 啊,怎麼可能會有其他的可能性?沒錯,以數學式來說的話,的確很難不是 pure function 。很可惜的是,我們在寫程式,卻通常不是這麼一回事,隨時隨地都在寫不 pure 的 function。
fun Int.dpToPx(): Int {
val density = Resources.getSystem().displayMetrics.density
return Math.round(this * density)
}
看看上面這個例子,他是一個 extension function,這個 function 主要的目的是要做單位換算,大家可以不用知道這些單位是什麼意思(但Android 工程師不能不知道),只要知道這個 function 主要計算的公式只有 this*density
就好,頂多再加上 Math.round
,一切都看似很單純。然而實際上,當 Android application 在執行時,這個值也永遠會是固定的,所以...這是一個 pure function 了嗎?
答案當然是 - 他不是一個 pure function。當你換不同手機來執行的時候,這個值就會不一樣了,為什麼呢?因為這個 function 相依了一個 global Singleton - Resources.getSystem() ,這邊的值會因為環境不同而有所改變,下面再列幾個例子:
// Pure functions
fun divide(a: Int, b: Int) = a / b
// 遞迴也可以是 Pure function
fun triangleArea(height: Int): Int {
return if (height == 1) {
1
} else {
height + triangleArea(height - 1)
}
}
// Non-Pure functions
// globalVariable 可能會被其他人改變
var globalVariable = 4
fun interactWithGlobal(a: Int) = a + globalVariable
// 也是一個常見的 case,Singleton 不在這個 function 的控制範圍
fun insertData(data: User): Boolean {
return UserRepository.getInstance().insert(data)
}
// Pure function
// 雖然不在 function 的控制範圍,但是由於是常數,所以輸入跟輸出是恆定的
const val constVariable = 5
fun interactWithConst(a: Int) = a + constVariable
在講到 Function programming 時常常會提到這概念,但不知道為什麼,一開始在學的時候卻怎麼都搞不懂。還有人說他只是 pure function 比較 fancy 的說法?這讓我更混亂了。但後來看了更多之後發現其實這是個很簡單的概念,讓我們先專注在他的“數學含義“。
每個人都背過99乘法表對吧?我們為什麼要去背他呢?因為這是一個很常見的計算,我們並不想要在做乘法的時候還要一個一個的用加法從頭計算,這實在是太浪費時間了。事實上,在這過程中,我們已經運用了 Referential transparency 的概念:
// 從以前學的數學中,我們知道下面這件事
5+5+5+5+5 = 5 * 5 = 25
// 在計算較複雜的算式時,可以利用已知的等式,來做“替換”
a = 5 + 5 + 5 + 5 + 5
b = 3 + 3 + 3 + 3
a + b = 5*5 + 3*4 // 將加法替換成乘法
= 25 + 12 // 對照 99 乘法表,可以直接換到實際數值
= 37
如果用更抽象一點的語言來說的話,如果我現在知道 a 可以推導到 b 這個結果,而 b 可以推導到 c的結果,自然而然的就可以從 a 推導到 c 。這件事對於“人腦”而言是非常自然的,這就是基本的邏輯推導。
讓我們來看看反例,很多人是在寫 javascript 的時候才第一次接觸到 functional programming ,但你知道嗎,這個程式語言的設計,處處與 functional programming 的原則起衝突!看看下面這張圖:
如果是符合 referential transparency 的話 ,"0" == []
應該要回傳 true 才對!
Mutable 跟 immutable 對於很多人來說不是一個新概念,在很多物件導向的書籍也有提到,但由於他在 functional programming 裡也是一個非常重要的觀念,在這邊針對他對 functional programming 的意義來做解說。
TL;DR - mutable 就是可變的變數,immutable 則是不可變的變數
class Point(var x: Int, var y: Int)
fun foo(point: Point): Int {
return point.x + 4
}
fun boo(point: Point): Int {
point.x = 100
return distance (point, Point(0, 0))
}
上方的範例中 boo 這個 function 改變了 point 的其中一個值,如果 foo 跟 boo 背後所用的 point 是同一個的話,這兩個 function 的執行順序將會大大的影響結果!同時,也自然不會是 pure function,以下提供 immutable 的版本:
//1
data class Point(val x: Int, val y: Int)
fun foo(point: Point): Int {
return point.x + 4
}
fun boo(point: Point): Int {
//2
val newPoint = point.copy(x = 100)
return distance (point, Point(0, 0))
}
val
,如此一來,可以防止有人不小心修改到裡面的值。Functional programming 在實務上其中一個最大的應用場景是:平行化運算。在平行運算中如果使用了共享的可變變數,是會發生很多災難的。
請各位看看下面這個例子:
class Account(val id: Int, val money: Money)
val accountA = Account(...)
請問 accountA 這個變數是 immutable 嗎?,如果不是,為什麼呢?答案將在下回分曉
Referencial Transparency 的部分好像跟我認知的有點不太一樣~ XD
我認知的 Referencial Transparency 是「一個運算式如果可以被它所回傳的值給取代,並且不影響整個程式的操作,則稱之為 Referencial Transparency。」而不是像上面所述的是 5 個 5 相加可以替換成別的算式 5 * 5
這樣~ XD
像是使用了 string 和 StringBuilder:
// string
val s1 = "abc".reverse
val s2 = s1.reverse
println(s1 + s2) // cbaabc
// 和直接代入的結果相同
val s1 = "abc".reverse
val s2 = ("abc".reverse).reverse
println(s1 + s2) // cbaabc
// StringBuilder
val s1 = new StringBuilder("abc").reverse
val s2 = s1.reverse
println(s1.append(s2)) // abcabc
// 和直接代入的結果不同。
// 因為 s1 做了 reverse 所得到的物件 reference
// 與最後 s2 做 reverse 時的物件 reference 是同一個所導致
// 如果 StringBuilder.reverse 不是透過狀態在變更,
// 而是另傳一個物件就不會有這個問題
val s1 = new StringBuilder("abc").reverse
val s2 = (new StringBuilder("abc").reverse).reverse
println(s1.append(s2)) // cbaabc
所以在上面 JS 部分舉的例子只能說 JS 的雙等號 ==
沒有值的遞移律關係,雖然與我們認知的等號含義不同,實際上應該還是符合 Referencial Transparency,因為假如我在程式碼裡面使用它:
let a = (0 == "0");
let b = (0 == []);
let c = ("0" == []);
console.log(a && b && c); // WTF, it's false!
// 但是符合 Referencial Transparency
console.log((0 == "0") && (0 == []) && ("0" == [])); // 一樣是 false TAT
當然說不定我的理解也是有錯XDDDDDD
如果有錯的地方,就再指證我沒有關係,不好意思XDDDDD
參考的資料是這裡:https://www.slideshare.net/shinolajla/taxonomy-ofscala/17-Referential_Transparency_Transparentval_example1_jamiereverseval
非常感謝你的回應!其實 5 * 5 => 5 + 5 + 5 + 5 + 5
那些是我自己推斷出來的,被這樣一說就開始找了許多相關文件來看XD
目前找到的資料顯示(包含 Wiki 以及其他很多人寫的文章),以 Computer Science 領域來說,你的說法比較恰當:「一個運算式如果可以被它所回傳的值給取代,並且不影響整個程式的操作,則稱之為 Referencial Transparency。」,但後來我發現在數學上的 Referencial Transparency,又有另一種講法『In maths, referential transparency is the property of expressions that can be replaced by other expressions having the same value without changing the result in any way.』,這樣顯示我的講法好像也說得通XD,這樣就讓我更混亂了,所以與其任其混亂,我覺得了解他對寫程式來說的意義比較重要,不管是可以被取代的只有“值”本身還是包含了“表示式”與“值”,都隱含這是我們人腦對於一個表示式的直覺,等號的左邊或右邊應該是要恆定的才是。
至於 JS 那個其實是我想開的一個小玩笑...如果冒犯了請見諒,想說有這個橋段大家比較有感覺XD
附上 reference: https://www.sitepoint.com/what-is-referential-transparency/
一詞多義真的很麻煩呀QwQ~ 感謝討論xD
JS 那段比較像是講 JS 的 ==
不符合相等性要求的 Transitive。
了解~看來這段還是不太適合放上來,但我目前就不更正了,讓大家看一下這串討論的時候也比較有 Context ,如果我在別的平台上貼文的話會注意的。